// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.

#include "pw_containers/inline_var_len_entry_queue.h"

#include <cstring>
#include <string_view>
#include <variant>

#include "pw_containers/test/generic_var_len_entry_queue_testing.h"
#include "pw_containers_private/inline_var_len_entry_queue_test_oracle.h"
#include "pw_span/span.h"
#include "pw_unit_test/framework.h"

namespace {

struct PushOverwrite {
  std::string_view data;
};
struct Push {
  std::string_view data;
};
struct TryPush {
  std::string_view data;
  bool expected;
};
struct Pop {};
struct Clear {};
struct SizeEquals {
  size_t expected;
};

using TestStep =
    std::variant<PushOverwrite, Push, TryPush, Pop, Clear, SizeEquals>;

// Copies an entry, which might be wrapped, to a single std::vector.
std::vector<std::byte> ReadEntry(const pw_InlineVarLenEntryQueue_Iterator& it) {
  auto entry = pw_InlineVarLenEntryQueue_GetEntry(&it);
  std::vector<std::byte> value(entry.size_1 + entry.size_2);
  EXPECT_EQ(value.size(),
            pw_InlineVarLenEntryQueue_Entry_Copy(
                &entry, value.data(), entry.size_1 + entry.size_2));
  return value;
}

// Declares a test that performs a series of operations on the C and C++
// versions of InlineVarLenEntryQueue and the "oracle" class, and checks that
// they match after every step.
template <size_t kMaxEntrySize>
void DataDrivenTest(pw::span<const TestStep> program) {
  pw::InlineVarLenEntryQueue<kMaxEntrySize> cpp_queue;
  PW_VARIABLE_LENGTH_ENTRY_QUEUE_DECLARE(c_queue, kMaxEntrySize);
  pw::containers::InlineVarLenEntryQueueTestOracle oracle(kMaxEntrySize);

  /* Check the queue sizes */
  static_assert(sizeof(cpp_queue) == sizeof(c_queue));
  ASSERT_EQ(cpp_queue.raw_storage().data(),
            reinterpret_cast<const std::byte*>(&cpp_queue));
  ASSERT_EQ(cpp_queue.raw_storage().size_bytes(),
            pw_InlineVarLenEntryQueue_RawStorageSizeBytes(c_queue));

  for (const TestStep& step : program) {
    /* Take the action */
    if (auto ow = std::get_if<PushOverwrite>(&step); ow != nullptr) {
      cpp_queue.push_overwrite(pw::as_bytes(pw::span(ow->data)));
      pw_InlineVarLenEntryQueue_PushOverwrite(
          c_queue, ow->data.data(), static_cast<uint32_t>(ow->data.size()));
      oracle.push_overwrite(pw::as_bytes(pw::span(ow->data)));
    } else if (auto push = std::get_if<Push>(&step); push != nullptr) {
      cpp_queue.push(pw::as_bytes(pw::span(push->data)));
      pw_InlineVarLenEntryQueue_Push(
          c_queue, push->data.data(), static_cast<uint32_t>(push->data.size()));
      oracle.push(pw::as_bytes(pw::span(push->data)));
    } else if (auto try_push = std::get_if<TryPush>(&step);
               try_push != nullptr) {
      ASSERT_EQ(try_push->expected,
                cpp_queue.try_push(pw::as_bytes(pw::span(try_push->data))));
      ASSERT_EQ(try_push->expected,
                pw_InlineVarLenEntryQueue_TryPush(
                    c_queue,
                    try_push->data.data(),
                    static_cast<uint32_t>(try_push->data.size())));
      if (try_push->expected) {
        oracle.push(pw::as_bytes(pw::span(try_push->data)));
      }
    } else if (std::holds_alternative<Pop>(step)) {
      cpp_queue.pop();
      pw_InlineVarLenEntryQueue_Pop(c_queue);
      oracle.pop();
    } else if (auto size = std::get_if<SizeEquals>(&step); size != nullptr) {
      const size_t actual = cpp_queue.size();
      ASSERT_EQ(actual, pw_InlineVarLenEntryQueue_Size(c_queue));
      ASSERT_EQ(oracle.size(), actual);
      ASSERT_EQ(size->expected, actual);
    } else if (std::holds_alternative<Clear>(step)) {
      cpp_queue.clear();
      pw_InlineVarLenEntryQueue_Clear(c_queue);
      oracle.clear();
    } else {
      FAIL() << "Unhandled case";
    }
    /* Check sizes */
    ASSERT_EQ(cpp_queue.size(), oracle.size());
    ASSERT_EQ(cpp_queue.size_bytes(), oracle.size_bytes());
    ASSERT_EQ(cpp_queue.max_size_bytes(), oracle.max_size_bytes());

    ASSERT_EQ(pw_InlineVarLenEntryQueue_Size(c_queue), oracle.size());
    ASSERT_EQ(pw_InlineVarLenEntryQueue_SizeBytes(c_queue),
              oracle.size_bytes());
    ASSERT_EQ(pw_InlineVarLenEntryQueue_MaxSizeBytes(c_queue),
              oracle.max_size_bytes());

    /* Compare the contents */
    auto oracle_it = oracle.begin();
    auto c_queue_it = pw_InlineVarLenEntryQueue_Begin(c_queue);

    const auto c_queue_end = pw_InlineVarLenEntryQueue_End(c_queue);
    uint32_t entries_compared = 0;

    for (auto entry : cpp_queue) {
      entries_compared += 1;

      ASSERT_EQ(*oracle_it, ReadEntry(c_queue_it));
      ASSERT_EQ(*oracle_it, std::vector<std::byte>(entry.begin(), entry.end()));

      ASSERT_NE(oracle_it, oracle.end());
      ASSERT_FALSE(
          pw_InlineVarLenEntryQueue_Iterator_Equal(&c_queue_it, &c_queue_end));

      ++oracle_it;
      pw_InlineVarLenEntryQueue_Iterator_Advance(&c_queue_it);
    }
    ASSERT_EQ(entries_compared, oracle.size());
    ASSERT_TRUE(
        pw_InlineVarLenEntryQueue_Iterator_Equal(&c_queue_it, &c_queue_end));
    ASSERT_EQ(oracle_it, oracle.end());
  }
}

#define DATA_DRIVEN_TEST(steps, max_entry_size)                                \
  TEST(InlineVarLenEntryQueue,                                                 \
       DataDrivenTest_##steps##_MaxSizeBytes##max_entry_size) {                \
    DataDrivenTest<max_entry_size>({steps, sizeof(steps) / sizeof(TestStep)}); \
  }                                                                            \
  static_assert(true, "use a semicolon")

constexpr TestStep kPop[] = {
    SizeEquals{0},
    PushOverwrite{""},
    SizeEquals{1},
    Pop{},
    SizeEquals{0},
};

DATA_DRIVEN_TEST(kPop, 0);  // Only holds one empty entry.
DATA_DRIVEN_TEST(kPop, 1);
DATA_DRIVEN_TEST(kPop, 6);

constexpr TestStep kOverwriteLargeEntriesWithSmall[] = {
    PushOverwrite{"12345"},
    PushOverwrite{"abcde"},
    PushOverwrite{""},
    PushOverwrite{""},
    PushOverwrite{""},
    PushOverwrite{""},
    PushOverwrite{""},
    PushOverwrite{""},
    SizeEquals{6},
    Pop{},
    Pop{},
    Pop{},
    Pop{},
    Pop{},
    Pop{},
    SizeEquals{0},
};
DATA_DRIVEN_TEST(kOverwriteLargeEntriesWithSmall, 5);
DATA_DRIVEN_TEST(kOverwriteLargeEntriesWithSmall, 6);
DATA_DRIVEN_TEST(kOverwriteLargeEntriesWithSmall, 7);

constexpr TestStep kOverwriteVaryingSizes012[] = {
    PushOverwrite{""},   PushOverwrite{""},   PushOverwrite{""},
    PushOverwrite{""},   PushOverwrite{""},   PushOverwrite{"1"},
    PushOverwrite{"2"},  PushOverwrite{""},   PushOverwrite{"3"},
    PushOverwrite{"4"},  PushOverwrite{""},   PushOverwrite{"5"},
    PushOverwrite{"6"},  PushOverwrite{"ab"}, PushOverwrite{"cd"},
    PushOverwrite{""},   PushOverwrite{"ef"}, PushOverwrite{"gh"},
    PushOverwrite{"ij"},
};
DATA_DRIVEN_TEST(kOverwriteVaryingSizes012, 2);
DATA_DRIVEN_TEST(kOverwriteVaryingSizes012, 3);

constexpr TestStep kOverwriteVaryingSizesUpTo4[] = {
    PushOverwrite{""},
    PushOverwrite{""},
    PushOverwrite{""},
    PushOverwrite{"1"},
    PushOverwrite{"2"},
    PushOverwrite{"3"},
    PushOverwrite{"ab"},
    PushOverwrite{"cd"},
    PushOverwrite{"ef"},
    PushOverwrite{"123"},
    PushOverwrite{"456"},
    PushOverwrite{"789"},
    PushOverwrite{"abcd"},
    PushOverwrite{"efgh"},
    PushOverwrite{"ijkl"},
    TryPush{"uhoh", false},
    Pop{},
    SizeEquals{0},
};
DATA_DRIVEN_TEST(kOverwriteVaryingSizesUpTo4, 4);
DATA_DRIVEN_TEST(kOverwriteVaryingSizesUpTo4, 5);
DATA_DRIVEN_TEST(kOverwriteVaryingSizesUpTo4, 6);

constexpr char kBigEntryBytes[196]{};

template <size_t kSizeBytes>
constexpr std::string_view kBigEntry(kBigEntryBytes, kSizeBytes);

constexpr TestStep kTwoBytePrefix[] = {
    PushOverwrite{kBigEntry<128>},
    PushOverwrite{kBigEntry<128>},
    PushOverwrite{kBigEntry<127>},
    PushOverwrite{kBigEntry<128>},
    PushOverwrite{kBigEntry<127>},
    SizeEquals{1},
    Pop{},
    SizeEquals{0},
};
DATA_DRIVEN_TEST(kTwoBytePrefix, 128);
DATA_DRIVEN_TEST(kTwoBytePrefix, 129);

constexpr TestStep kClear[] = {
    Push{"abcdefg"},
    PushOverwrite{""},
    PushOverwrite{""},
    PushOverwrite{"a"},
    PushOverwrite{"b"},
    Clear{},
    SizeEquals{0},
    Clear{},
};
DATA_DRIVEN_TEST(kClear, 7);
DATA_DRIVEN_TEST(kClear, 100);

constexpr TestStep kTryPushMaxSize5[] = {
    TryPush{"", true},
    TryPush{"", true},
    TryPush{"", true},
    TryPush{"", true},
    TryPush{"", true},
    TryPush{"", true},  // max_size_bytes() of 5 => up to 6 empty entries
    TryPush{"", false},
    TryPush{"1", false},
    Clear{},
    TryPush{"12345", true},
    TryPush{"", false},
};
DATA_DRIVEN_TEST(kTryPushMaxSize5, 5);

constexpr TestStep kPushPopLargeEntry[] = {
    Push{kBigEntry<196>},
    TryPush{kBigEntry<196>, false},
    Pop{},
    Push{kBigEntry<196>},
    TryPush{"", true},
    Pop{},
    TryPush{"1", true},
    TryPush{kBigEntry<196>, true},
    TryPush{"12", true},
    Pop{},
    Pop{},
    Pop{},
    TryPush{kBigEntry<196>, true},
    TryPush{kBigEntry<196>, false},
};
DATA_DRIVEN_TEST(kPushPopLargeEntry, 255);
DATA_DRIVEN_TEST(kPushPopLargeEntry, 256);
DATA_DRIVEN_TEST(kPushPopLargeEntry, 257);

TEST(InlineVarLenEntryQueue, DeclareMacro) {
  PW_VARIABLE_LENGTH_ENTRY_QUEUE_DECLARE(queue, 123);

  constexpr size_t kArraySizeBytes =
      123 + 1 /*prefix*/ + 1 /* end */ + 3 /* round up */ +
      PW_VARIABLE_LENGTH_ENTRY_QUEUE_HEADER_SIZE_UINT32 * 4;
  static_assert(sizeof(queue) == kArraySizeBytes);
  EXPECT_EQ(pw_InlineVarLenEntryQueue_RawStorageSizeBytes(queue),
            kArraySizeBytes - 3 /* padding isn't included */);

  EXPECT_EQ(pw_InlineVarLenEntryQueue_MaxSizeBytes(queue), 123u);
  EXPECT_EQ(pw_InlineVarLenEntryQueue_SizeBytes(queue), 0u);
  EXPECT_TRUE(pw_InlineVarLenEntryQueue_Empty(queue));
}

TEST(InlineVarLenEntryQueue, InitializeExistingBuffer) {
  constexpr size_t kArraySize =
      10 + PW_VARIABLE_LENGTH_ENTRY_QUEUE_HEADER_SIZE_UINT32;
  uint32_t queue[kArraySize];
  pw_InlineVarLenEntryQueue_Init(queue, kArraySize);

  EXPECT_EQ(pw_InlineVarLenEntryQueue_RawStorageSizeBytes(queue),
            sizeof(queue));
  EXPECT_EQ(pw_InlineVarLenEntryQueue_MaxSizeBytes(queue),
            sizeof(uint32_t) * 10u - 1 /*prefix*/ - 1 /*end*/);
  EXPECT_EQ(pw_InlineVarLenEntryQueue_SizeBytes(queue), 0u);
  EXPECT_EQ(pw_InlineVarLenEntryQueue_Size(queue), 0u);
  EXPECT_TRUE(pw_InlineVarLenEntryQueue_Empty(queue));
}

TEST(InlineVarLenEntryQueue, MaxSizeElement) {
  // Test max size elements for a few sizes. Commented out statements fail an
  // assert because the elements are too large.
  PW_VARIABLE_LENGTH_ENTRY_QUEUE_DECLARE(q16, 126);
  PW_VARIABLE_LENGTH_ENTRY_QUEUE_DECLARE(q17, 127);
  PW_VARIABLE_LENGTH_ENTRY_QUEUE_DECLARE(q18, 128);
  PW_VARIABLE_LENGTH_ENTRY_QUEUE_DECLARE(q19, 129);

  pw_InlineVarLenEntryQueue_PushOverwrite(q16, kBigEntryBytes, 126);
  pw_InlineVarLenEntryQueue_PushOverwrite(q17, kBigEntryBytes, 126);
  pw_InlineVarLenEntryQueue_PushOverwrite(q18, kBigEntryBytes, 126);
  pw_InlineVarLenEntryQueue_PushOverwrite(q19, kBigEntryBytes, 126);

  // pw_InlineVarLenEntryQueue_PushOverwrite(q16, kBigEntryBytes, 127);
  pw_InlineVarLenEntryQueue_PushOverwrite(q17, kBigEntryBytes, 127);
  pw_InlineVarLenEntryQueue_PushOverwrite(q18, kBigEntryBytes, 127);
  pw_InlineVarLenEntryQueue_PushOverwrite(q19, kBigEntryBytes, 127);

  // pw_InlineVarLenEntryQueue_PushOverwrite(q16, kBigEntryBytes, 128);
  // pw_InlineVarLenEntryQueue_PushOverwrite(q17, kBigEntryBytes, 128);
  pw_InlineVarLenEntryQueue_PushOverwrite(q18, kBigEntryBytes, 128);
  pw_InlineVarLenEntryQueue_PushOverwrite(q19, kBigEntryBytes, 128);

  // pw_InlineVarLenEntryQueue_PushOverwrite(q16, kBigEntryBytes, 129);
  // pw_InlineVarLenEntryQueue_PushOverwrite(q17, kBigEntryBytes, 129);
  // pw_InlineVarLenEntryQueue_PushOverwrite(q18, kBigEntryBytes, 129);
  pw_InlineVarLenEntryQueue_PushOverwrite(q19, kBigEntryBytes, 129);

  EXPECT_EQ(pw_InlineVarLenEntryQueue_Size(q16), 1u);
  EXPECT_EQ(pw_InlineVarLenEntryQueue_Size(q17), 1u);
  EXPECT_EQ(pw_InlineVarLenEntryQueue_Size(q18), 1u);
  EXPECT_EQ(pw_InlineVarLenEntryQueue_Size(q19), 1u);
}

TEST(InlineVarLenEntryQueueClass, InitializeExistingBuffer) {
  constexpr size_t kArraySize =
      10 + PW_VARIABLE_LENGTH_ENTRY_QUEUE_HEADER_SIZE_UINT32;
  uint32_t queue_array[kArraySize]{50, 50, 99};
  pw::InlineVarLenEntryQueue<>& queue =
      pw::InlineVarLenEntryQueue<>::Init(queue_array, kArraySize);

  EXPECT_EQ(queue.raw_storage().data(),
            reinterpret_cast<const std::byte*>(queue_array));
  EXPECT_EQ(queue.raw_storage().size_bytes(), sizeof(queue_array));
  EXPECT_EQ(queue.max_size_bytes(),
            sizeof(uint32_t) * 10u - 1 /*prefix*/ - 1 /*end*/);
  EXPECT_EQ(queue.size_bytes(), 0u);
  EXPECT_EQ(queue.size(), 0u);
  EXPECT_TRUE(queue.empty());
}

TEST(InlineVarLenEntryQueueClass, Construct_Constexpr) {
  constexpr pw::InlineVarLenEntryQueue<127> queue(pw::kConstexpr);
  EXPECT_TRUE(queue.empty());
  EXPECT_EQ(queue.max_size(), 128u);
  EXPECT_EQ(queue.size(), 0u);
}

// Include tests of GenericVarLenEntryQueue, using a BasicInlineVarLenEntryQueue
// factory.
class BasicInlineVarLenEntryQueueTest
    : public pw::containers::test::GenericVarLenEntryQueueTest<
          BasicInlineVarLenEntryQueueTest> {
 public:
  template <typename T, size_t kMaxSizeBytes>
  pw::BasicInlineVarLenEntryQueue<T, kMaxSizeBytes> MakeQueue() {
    return pw::BasicInlineVarLenEntryQueue<T, kMaxSizeBytes>();
  }
};
PW_GENERIC_VAR_LEN_ENTRY_QUEUE_TESTS(BasicInlineVarLenEntryQueueTest);

}  // namespace
